package db

import "github.com/boltdb/bolt"
import "fmt"
import "time"
import "io"
import "encoding/csv"
import "strconv"

type nid int64

func (n nid) NodeId() nid {
	return n
}

func (n nid) NodeIds() []nid {
	return []nid{n}
}

func (n nid) toBytes() []byte {
	return i64tob(int64(n))
}

type nodeList struct {
	names []string
	nids []nid
}

func (n *nodeList) NodeNames() []string {
	return n.names
}

func (n *nodeList) NodeIds() []nid {
	return n.nids
}

type NodeId interface {
	NodeId() nid
	NodeIds() []nid
}

type NodeIds interface {
	NodeIds() []nid
}

type Nodes interface {
	NodeIds
	NodeNames() []string
}

func FilterNodes(ns Nodes, f func(name string) bool) Nodes {
	names := ns.NodeNames()
	out := nodeList{
		names: make([]string, 0, len(names)),
		nids: make([]nid, 0, len(names)),
	}
	for i, n := range ns.NodeIds() {
		if f(names[i]) {
			out.names = append(out.names, names[i])
			out.nids = append(out.nids, n)
		}
	}
	return &out
}

type node struct {
	nid
	name []byte
}

func (n node) NodeName() string {
	return string(n.name)
}

type Node interface {
	NodeId
	NodeName() string
}

// DB is a wrapper around the database that provides the high level interface.
type DB struct {
	path    string
	db      *bolt.DB
}

// OpenDbForUrl creates a DB handle for indexing the specified URL.
func OpenDbForUrl(path string, url string) (*DB, error) {
	d := DB{path: path}

	if db, err := bolt.Open(d.path, 0666); err != nil {
		return nil, fmt.Errorf("error opening bolt db at path '%s': %v", path, err)
	} else {
		d.db = db
	}

	err := d.db.Update(func(tx *bolt.Tx) error {
		_, err := OpenOrCreateNodeName(tx, "")
		if err != nil {
			return err
		}
		_, err = OpenOrCreateNodeQueue(tx, "")
		if err != nil {
			return err
		}
		_, err = tx.CreateBucketIfNotExists([]byte("edge"))
		return err
	})
	if err != nil {
		return nil, fmt.Errorf("can't init db: %s", err)
	}

	return &d, nil
}

// Close closes the DB.
func (d *DB) Close() error {
	return d.db.Close()
}

type viewTx struct {
	*NodeNameQueue
	*NodeGraph
}

func newViewTx(tx *bolt.Tx) (*viewTx, error) {
	vtx := &viewTx{}
	nq, err := OpenNodeQueue(tx, "")
	if err != nil {
		return nil, err
	}
	nn, err := OpenNodeName(tx, "")
	if err != nil {
		return nil, err
	}

	vtx.NodeNameQueue = &NodeNameQueue{nn, nq}
	bedge :=  &EdgeBucket{openBucketTx(tx, "edge")}
	vtx.NodeGraph = &NodeGraph{nn, bedge}
	return vtx, nil
}

type updateTx struct {
	viewTx
}

func (d *DB) view(f func(utx *viewTx) error) error {
	return d.db.View(func(tx *bolt.Tx) error {
		vtx, err := newViewTx(tx)
		if err != nil {
			return err
		}
		return f(vtx)
	})
}

func (d *DB) update(f func(utx *updateTx) error) error {
	return d.db.Update(func(tx *bolt.Tx) error {
		vtx, err := newViewTx(tx)
		if err != nil {
			return err
		}
		return f(&updateTx{viewTx: *vtx})
	})
}

// This routine looks up all the names to get their corresponding nids.
// Any not present yet are added in a subsequent update transaction.
func (d *DB) getOrAddNids(names [][]byte) ([]nid, error) {
	nids := make([]nid, len(names))
	var firstMissing int
	err := d.view(func(vtx *viewTx) error {
		firstMissing = vtx.NodeNameQueue.NodeName.GetNidsPresent(names, nids)
		return nil
	})
	if err != nil {
		return nil, err
	}
	if firstMissing == -1 {
		return nids, nil
	}

	err = d.update(func(utx *updateTx) error {
		for i := firstMissing; i < len(names); i++ {
			if nids[i] == 0 {
				nids[i], err = utx.NodeNameQueue.NodeName.Add(names[i])
				if err != nil {
					return err
				}
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return nids, nil
}

// GetNodes returns the NodeIds corresponding to nodes, inserting any that do
// not alreayd exist in the db.
func (d *DB) GetNodes(strs []string) (Nodes, error) {
	nodesb := make([][]byte, len(strs))
	for i, s := range strs {
		nodesb[i] = []byte(s)
	}
	linknids, err := d.getOrAddNids(nodesb)
	if err != nil {
		return nil, err
	}
	return &nodeList{strs, linknids}, nil
}

func (d *DB) setEdges(from nid, to []nid) error {
	return d.update(func(utx *updateTx) error {
		return utx.SetEdges(from, to)
	})
}

// SetEdges assigns links as the edgelist of name.  
// TODO we don't validate that these nodeids exist... maybe we should to
// ensure consistency of the db.
func (d *DB) SetEdges(from NodeId, to NodeIds) error {
	if err := d.setEdges(from.NodeId(), to.NodeIds()); err != nil {
		return err
	}
	return nil
}

// Schedule the given nodes to be visited any time after time t if they aren't
// already scheduled.
func (d *DB) SetRefreshTimeIfUnset(nodes NodeIds, t time.Time) error {
	return d.update(func(utx *updateTx) error {
		un := t.UnixNano()
		for _, n := range nodes.NodeIds() {
			if _, err := utx.NodeQueue.GetOrd(n); err != nil {
				if err := utx.NodeQueue.Add(n, un); err != nil {
					return err
				}
			}
		}
		return nil
	})
}

// Schedule the given nodes to be visited any time after time t.
func (d *DB) SetRefreshTime(nodes NodeIds, t time.Time) error {
	return d.update(func(utx *updateTx) error {
		un := t.UnixNano()
		for _, n := range nodes.NodeIds() {
			if err := utx.UpdateOrder(n, un); err != nil {
				return err
			}
		}
		return nil
	})
}

// Get the first item in the queue if any.  It is not considered an error for
// the queue to be empty, i.e. it is possible for all the return values to be zero-values.
func (d *DB) GetFirst() (Node, time.Time, error) {
	var first []NidNameOrd
	err := d.view(func(vtx *viewTx) error {
		first = vtx.NodeNameQueue.FirstN(1)
		return nil
	})
	if err != nil || len(first) == 0 {
		return nil, time.Time{}, err
	}
	return node{first[0].n, first[0].name}, time.Unix(0, first[0].ord), nil
}

// writeQueueCsv dumps the queue, emitting csv of the form timeRFC3339,nid,name.
func (vtx *viewTx) writeQueueCsv(w io.Writer) error {
	csvw := csv.NewWriter(w)
	return vtx.NodeNameQueue.ForEach(func(nno NidNameOrd) error {
		rec := make([]string, 3)
		rec[0] = time.Unix(0, nno.ord).Format(time.RFC3339)
		rec[1] = string(strconv.AppendInt([]byte{}, int64(nno.n), 10))
		rec[2] = string(nno.name)

		if err := csvw.Write(rec); err != nil {
			return err
		}
		return nil
	})
}

// WriteQueueCsv dumps the queue, emitting csv of the form timeRFC3339,nid,name.
func (d *DB) WriteQueueCsv(w io.Writer) error {
	return d.view(func(vtx *viewTx) error {
		return vtx.writeQueueCsv(w)
	})
}

// writeEdgesCsv dumps the edgelists, emitting csv of the form nid,name,"list,of,child,nids"
func (vtx *viewTx) writeEdgesCsv(w io.Writer) error {
	csvw := csv.NewWriter(w)
	return vtx.NodeGraph.ForEachNodeEdges(func(k nid, v []nid) error {
		rec := make([]string, 3)
		rec[0] = string(strconv.AppendInt([]byte{}, int64(k), 10))
		name, err := vtx.NodeGraph.GetName(k)
		if err != nil {
			return err
		}
		rec[1] = string(name)
		rec[2] = nidsToCsv(v)

		if err := csvw.Write(rec); err != nil {
			return err
		}
		return nil
	})
}

// WriteEdgesCsv dumps the edgeslists, emitting csv of the form nid,name,"list,of,child,nids"
func (d *DB) WriteEdgesCsv(w io.Writer) error {
	return d.view(func(vtx *viewTx) error {
		return vtx.writeEdgesCsv(w)
	})
}

// writeEdgePairsCsv dumps the edgelists, emitting csv of the form parentUrl,childUrl
func (vtx *viewTx) writeEdgePairsCsv(w io.Writer) error {
	csvw := csv.NewWriter(w)
	return vtx.NodeGraph.ForEachNodeEdges(func(k nid, v []nid) error {
		rec := make([]string, 2)
		parName, err := vtx.NodeGraph.GetName(k)
		if err != nil {
			return err
		}
		rec[0] = string(parName)

		for _, childn := range v {
			childName, err := vtx.NodeGraph.GetName(childn)
			if err != nil {
				return err
			}
			rec[1] = string(childName)
			if err := csvw.Write(rec); err != nil {
				return err
			}
		}

		return nil
	})
}

// writeEdgePairsCsv dumps the edgelists, emitting csv of the form parentUrl,childUrl
func (d *DB) WriteEdgePairsCsv(w io.Writer) error {
	return d.view(func(vtx *viewTx) error {
		return vtx.writeEdgePairsCsv(w)
	})
}

